<template>
    <div class="upload">
      <div ref="topRef" class="top">
        <el-upload :http-request="upload" drag :show-file-list="false" multiple>
          <el-icon class="el-icon--upload"><upload-filled /></el-icon>
          <div class="el-upload__text">拖拽上传<em>点击上传</em></div>
          <template #tip>
            <div v-show="isShowBtn"
              style="display: flex;justify-content: space-between; align-items: center;margin-top:20px">
              <div class="el-upload__tip" style="margin: 10px 0;font-size: 14px; font-weight: bold"><span
                  style="color: ;">并发数: {{ pool.length }}</span> -
                <span style="color: ;">上传完成数: {{ finish
                }} / {{ all }}</span> -
                失败数量: <span style="color:red"> {{
                  failList.length
                }}</span>
  
              </div>
              <div><el-button style="margin:0px 0" @click="pause" type="primary">{{ btnStatus
              }}</el-button>
  
              </div>
            </div>
  
            <el-progress v-show="isShowBtn" :status="status" :indeterminate="sf" :striped="sf" :striped-flow="sf"
              :percentage="percentage" :color="customColorMethod" />
          </template>
        </el-upload>
  
      </div>
      <div>
        <el-scrollbar style="margin: 20px;" :height="mainHeight + 'px'">
          <FileCom @update:upload="sonUpload" :db="db" v-for="  e   in   initDownLoadInfo  " :item="e" :key="e.hash">
          </FileCom>
        </el-scrollbar>
      </div>
  
    </div>
  </template>
  
  <script setup lang="ts">
  import { ref, watch, getCurrentInstance, nextTick, onMounted, onUnmounted } from "vue";
  import { sFetch } from "./utils/request";
  import { UploadFilled } from "@element-plus/icons-vue";
  import { UploadProgressEvent } from "element-plus";
  import { UploadAjaxError } from "element-plus/es/components/upload/src/ajax.mjs";
  import sparkMd5 from "spark-md5";
  import FileCom from "./components/file.vue"
  const { proxy } = getCurrentInstance() as any
  const fileName = ref<string>("")
  const suffixValue = ref<string>("")
  const pool = ref<any>([])
  const sf = ref(false)
  const status = ref<'success' | 'exception' | 'warning' | ''>("")
  const all = ref<number>(0)
  const percentage = ref<number>(0)
  const finish = ref<number>(0)
  const db = ref<any>("")
  const pNumber = ref<any>(0)
  const sliceMax = ref<number>(0)
  type IinitDownLoadInfo = {
    per: number
    hash: string
    hz: string,
    name: string,
    cindex: number,
    max: number
  }
  onMounted(() => {
    window.addEventListener('resize', getMainHeight);
  })
  onUnmounted(() => {
    window.addEventListener('resize', getMainHeight);
  })
  
  const getMainHeight = () => {
    nextTick(() => {
      console.log(proxy);
      mainHeight.value = window.innerHeight - proxy.$refs.topRef.offsetHeight - 116
    })
  }
  const isPause = ref<boolean>(true)
  const btnStatus = ref<string>("暂停")
  const isShowBtn = ref<boolean>(false)
  watch(() => [percentage.value, isPause.value], (arr) => {
    if (arr[0] == 100 || arr[0] == 0) {
      isShowBtn.value = false
    } else {
      isShowBtn.value = true
      if (arr[1] == true) {
        btnStatus.value = '暂停'
      } else {
        btnStatus.value = '继续'
      }
    }
    getMainHeight()
  }, {
    deep: true,
    immediate: true
  })
  
  const initDownLoadInfo = ref<IinitDownLoadInfo[]>([])
  const sonUpload = (params: IinitDownLoadInfo) => {
    cx(params.hash, params.name, params.hz)
    all.value = +params.max
    finish.value = params.cindex
  }
  
  const getFileList = async () => {
    const res = await sFetch(
      "http://localhost:3000/getFileInfo",
      "get",
    );
    if (res.code === 200) {
      if (res.data.length !== 0) {
        initDownLoadInfo.value = res.data.map((e: IinitDownLoadInfo) => {
          return {
            name: e.name,
            max: e.max,
            hash: e.hash,
            cindex: e.cindex,
            hz: e.hz
          }
        })
      }
  
    }
  }
  const mainHeight = ref<number>(0)
  const init = () => {
    // 打开数据库
    var request = window.indexedDB.open('fileDb', 4);
    request.onerror = function () {
      alert("无法打开数据库");
    };
    request.onsuccess = function () {
      db.value = request.result;
    };
    request.onupgradeneeded = function (event: any) {
      db.value = event.target.result;
      console.log("数据库升级成功");
      var objectStore = "" as any
      if (!db.value.objectStoreNames.contains('fileBase64')) {
        objectStore = db.value.createObjectStore('fileBase64', {
          keyPath: 'hash'
        });
      }
      //     objectStore.createIndex('hash', 'hash', { unique: true }) 中的第一个参数'hash'表示要创建的索引的名称。
      // 第二个参数'hash'表示要被索引的属性名称，也就是存储在对象中的哪个字段需要被索引。
      // { unique: true } 表示这个索引的值必须是唯一的，也就是说如果有两个对象具有相同的'hash'属性值，那么将会出现错误。
      objectStore.createIndex('hash', 'hash', { unique: true });
    }
  
    getMainHeight()
    getFileList()
  }
  init()
  
  
  const add = (data: any) => {
    var request = db.value.transaction(['fileBase64'], 'readwrite') //新建事务，readwrite, readonly(默认), versionchange 
      .objectStore('fileBase64') //拿到IDBObjectStore 对象
      .add(data);
    request.onsuccess = function (success: any) {
  
      console.log('数据写入成功', success.type);
    }
    request.onerror = function (err: any) {
      console.log('数据写入失败', err.target.error.message);
    }
    request.onabort = function () {
      console.log('事务回滚');
    }
  }
  const getByKey = (hash: string) => {
    return new Promise<any>((resolve, reject) => {
      var transaction = db.value.transaction(['fileBase64']);
      var objectStore = transaction.objectStore('fileBase64');
      var index = objectStore.index('hash');
      var req = index.get(hash);
      req.onsuccess = function (e: any) {
        var result = e.target.result;
        if (result) {
          resolve(result)
        } else {
          reject("无此数据")
          console.log('无此数据')
        }
      }
    })
  }
  // const get = (key: string | number) => {
  //   var transaction = db.value.transaction(['fileBase64']);
  //   var objectStore = transaction.objectStore('fileBase64');
  //   var request = objectStore.get(key); //传主键
  //   request.onerror = function () {
  //     console.log('事务失败');
  //   };
  //   request.onsuccess = function () {
  //     if (request.result) {
  //       console.log(request.result)
  //     } else {
  //       console.log('未获得数据记录');
  //     }
  //   };
  // }
  // function update() {
  //   var request = db.value.transaction(['fileBase64'], 'readwrite').objectStore('fileBase64').put({
  //     hash: 1,
  //     name: '书剑恩仇录2',
  //   });
  //   request.onsuccess = function () {
  //     console.log('数据更新成功');
  //   }
  //   request.onerror = function () {
  //     console.log('数据更新失败');
  //   }
  // }
  
  // function remove() {
  //   var request = db.value.transaction(['fileBase64'], 'readwrite')
  //     .objectStore('fileBase64')
  //     .delete(1);
  //   request.onsuccess = function () {
  //     console.log('数据删除成功');
  //   };
  // }
  // function clear() {
  //   var request = db.value.transaction(['fileBase64'], 'readwrite')
  //     .objectStore('fileBase64')
  //     .clear();
  //   request.onsuccess = function () {
  //     console.log('数据清除成功');
  //   };
  // }
  function base64toBlob(base64Data: string, contentType?: string) {
    contentType = contentType || '';
    var sliceSize = 1024;
    var byteCharacters = atob(base64Data);
    var byteArrays = [];
  
    for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
      var slice = byteCharacters.slice(offset, offset + sliceSize);
  
      var byteNumbers = new Array(slice.length);
      for (var i = 0; i < slice.length; i++) {
        byteNumbers[i] = slice.charCodeAt(i);
      }
  
      var byteArray = new Uint8Array(byteNumbers);
      byteArrays.push(byteArray);
    }
    var blob = new Blob(byteArrays, { type: contentType });
    return blob;
  }
  
  const blobToBase64 = (blob: Blob) => {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => {
        const base64Data = reader.result && (reader.result as string).split(',')[1];
        resolve(base64Data);
      };
      reader.onerror = (error) => {
        reject(error);
      };
      reader.readAsDataURL(blob);
    });
  }
  const getLocalSplice = async (hash: string, index: number) => {
    let res = await getByKey(hash) as {
      hash: string,
      data: any[]
    }
    let sss = JSON.parse(JSON.stringify(res.data))
    let subList = [] as any
    sss.forEach((e: any, i: number) => {
      if (i >= index) {
        let obj = {
          hash: e.hash,
          blob: base64toBlob(e.base64)
        }
        subList.push(obj)
      }
    });
    return subList
  }
  
  
  let failList = ref([]) as any;   // 失败列表
  const reset = () => {
    pool.value = []
    finish.value = 0
    failList.value = []
    sf.value = false
    status.value = ""
    all.value = 0
    percentage.value = 0
    isPause.value = true
  }
  
  
  const customColorMethod = (percentage: number) => {
    let arrobj = {
      10: "#FF6347",
      20: "#FF7F50",
      30: "#FFA500",
      40: "#FFD700",
      50: "#FFEC8B",
      60: "#ADFF2F",
      70: "#7CFC00",
      80: "#00FF7F",
      90: "#00BFFF",
      100: "#9370DB",
    }
    if (percentage < 10) {
      return arrobj[10]
    }
    if (percentage < 20) {
      return arrobj[20]
    }
    if (percentage < 30) {
      return arrobj[30]
    }
    if (percentage < 40) {
      return arrobj[40]
    }
    if (percentage < 50) {
      return arrobj[50]
    }
    if (percentage < 60) {
      return arrobj[60]
    }
    if (percentage < 70) {
      return arrobj[70]
    }
    if (percentage < 80) {
      return arrobj[80]
    }
    if (percentage < 90) {
      return arrobj[90]
    }
    if (percentage < 100) {
      return arrobj[100]
    }
  
  }
  
  const cx = async (hash: string, name: string, hz: string) => {
    console.log(`${hash}--${name}--${hz}`);
    const res = await sFetch(
      "http://localhost:3000/test",
      "get",
      {
        name: `${hash}--${name}--${hz}`
      }
    );
    if (res.code === 200) {
      let sss = await getLocalSplice(res.data.hash, res.data.index)
      fileHash.value = res.data.hash
      fileName.value = res.data.name
      suffixValue.value = res.data.hz
      pNumber.value = res.data.index
      uploadFileChunks(sss, true)
    }
  }
  
  
  interface UploadRequestOptions {
    action: string;
    method: string;
    data: Record<string, string | Blob | [string | Blob, string]>;
    filename: string;
    file: File;
    headers: Headers | Record<string, string | number | null | undefined>;
    onError: (evt: UploadAjaxError) => void;
    onProgress: (evt: UploadProgressEvent) => void;
    onSuccess: (response: any) => void;
    withCredentials: boolean;
  }
  const sliceSize = 1024 * 1024;
  const fileHash = ref<string>("");
  // const fileName = ref<string>("");
  const maxUploadNumber = 6;
  //分片
  const slice = async (file: File) => {
    if (!file) alert('无文件');
    const fileChunks = []
    let suffix = (file.name as any).match(/\.[^.]+$/g)[0].replace(/^./gi, '')
    let filename = (file.name as any).match(/^.+(?=\.)/)[0]
  
    fileName.value = filename
    suffixValue.value = suffix
    let fileLength = Math.ceil(file.size / sliceSize)
    all.value = fileLength
    let cur = 0;
    for (let i = 0; i < fileLength; i++) {
  
      let res = await blobToBase64(file.slice(cur, cur + sliceSize))
      fileChunks.push({
        hash: i,
        blob: file.slice(cur, cur + sliceSize),
        base64: res
      })
      cur += sliceSize
    }
    return fileChunks
  }
  
  const pause = async () => {
  
    if (isPause.value) {
      ElMessage.success(1)
      const wait = new Promise((_resolve, reject) => {
        reject('请求超时')
      }) as any
      pool.value.unshift(wait)
      setTimeout(() => {
        getFileList()
      }, 1000);
    } else {
      ElMessage.success(2)
      cx(fileHash.value, fileName.value, suffixValue.value)
    }
    isPause.value = !isPause.value
  
  }
  
  //上传分片
  const uploadFileChunks = async (list: { hash: number, blob: Blob }[], isReUpload: boolean = false) => {
    sf.value = true
    if (list.length === 0) {
      sf.value = false
      // 所有分片上传完成，通知合并
      const res = await sFetch(
        "http://localhost:3000/",
        "get",
        {
          name: `${fileHash.value}--${fileName.value}||${suffixValue.value}`
        }
      );
      if (res.code === 200) {
        ElMessage.success(res.msg)
        getFileList()
  
      } else {
        ElMessage.error(res.msg)
      }
      return
    }
  
  
    for (let i = 0; i < list.length; i++) {
      const blobName = `${fileHash.value}//${fileName.value}--${list[i].hash}||${suffixValue.value}`;
      const blobFile = new File([list[i].blob], blobName);
      let formData = new FormData();
      formData.append('file', blobFile);
      formData.append('sliceMax', sliceMax.value as any);
      const task = sFetch(
        "http://localhost:3000/",
        "post",
        formData,
        true
      );
      task.then(res => {
        if (res.code === 200) {
          // 从并发池中移除已经完成的请求
          let index = pool.value.findIndex((t: any) => t === task);
          pool.value.splice(index);
          finish.value++;
  
        } else {
          status.value = "warning"
          failList.value.push(list[i]);
  
        }
      }).catch(() => {
        status.value = "exception"
        failList.value.push(list[i]);
  
  
      }).finally(() => {
        percentage.value = Number((parseFloat((finish.value / all.value).toFixed(2)) * 100).toFixed(2))
        //上传完毕后 如果有失败的重新上传
        if (isReUpload) {
          if (finish.value - pNumber.value === list.length) {
            status.value = 'success'
            if (failList.value.length !== 0) {
              ElMessage.error('重新上传错误分片')
            }
            uploadFileChunks(failList.value);
          }
        } else {
          if (finish.value === list.length) {
            status.value = 'success'
            if (failList.value.length !== 0) {
              ElMessage.error('重新上传错误分片')
            }
            uploadFileChunks(failList.value);
          }
        }
  
      });
      pool.value.push(task);
  
      if (pool.value.length === maxUploadNumber) {
        // 等待 线程池 完成任务
        try {
          await Promise.race(pool.value);
        } catch (error) {
          ElMessage.error(JSON.stringify(error))
          if (error === "请求超时") {
            pool.value = []
            break;
          }
  
        }
  
      }
    }
  };
  
  
  
  
  //计算hash
  const cumulateHash = (blobs: { hash: number, blob: Blob }[]) => {
    return new Promise<string>((resolve) => {
      const result = [] as Blob[];
      const spark = new sparkMd5.ArrayBuffer();
      const fileReader = new FileReader();
      blobs.forEach((item: any, index: number) => {
        if (index === 0 || index === blobs.length - 1) {
          result.push(item);
        } else {
          //取文件前两个字符 最后两个字符 中间两个字符
          result.push(item.blob.slice(0, 2));
          result.push(item.blob.slice(sliceSize / 2 - 1, sliceSize / 2 + 1));
          result.push(item.blob.slice(sliceSize - 2, sliceSize));
        }
      });
      fileReader.readAsArrayBuffer(new Blob(result));
      fileReader.onload = (e: any) => {
        spark.append((e.target as any).result);
        resolve(spark.end());
      };
    });
  };
  
  
  
  const upload = async (e: UploadRequestOptions) => {
    reset()
    let blobs = await slice(e.file);
    sliceMax.value = blobs.length
    // console.log(blobs, 'blobs');
    // ElMessage.error(`当前切片数: ` + JSON.stringify(blobs.length));
    let res = await cumulateHash(blobs)
    fileHash.value = res;
    add({
      hash: res, data: blobs.map(e => {
        return {
          hash: e.hash,
          base64: e.base64
        }
      })
    })
    uploadFileChunks(blobs)
  
  
  
    // tip
    //3 断点传续
    //4 文件夹上传
    //5 数据库redis
  
  
  
  
  
  
  
  };
  </script>
  
  <style scoped lang="scss">
  .upload {
    width: 50vw;
    height: auto;
    margin: 30px auto;
  }
  </style>
  